package ch.epfl.lara.matcheck.ast.parser;

import ch.epfl.lara.matcheck.Report
import ch.epfl.lara.matcheck.types._

import scala.collection.mutable.{Map,HashMap}


abstract class SyntaxAnalyzer extends Predefined {
  import Constants.UNAPPLY

  // Global scope of classes and objects
  type ClassScope = Map[String,ClassSymbol]
  type ObjectScope = Map[String,ObjectSymbol]
  
  val class_scope: ClassScope = HashMap.empty + ((ROOT, ROOT_SYMBOL))
  val object_scope: ObjectScope = HashMap.empty
  
  // For local scope of variables
  type VarScope = scala.collection.immutable.Map[String,VarSymbol]
  
  // Empty immutable map 
  val empty_var_scope: VarScope = scala.collection.immutable.HashMap.empty 
  
  
  
  
  /**
   * <p>
   *	Check that the program is syntactically correct.
   * </p>
   *
   * @param program: The source program.
   */
  protected def analyzeProgram(program: Program) {
    val Program(_,_,objs,cls) = program
    val declared_classes = ROOT :: cls.map(_.name.getName)
    analyzeObjectDecl(objs)
    analyzeClassesDecl(declared_classes,cls)
    cls.foreach(analyzeClassMemberDecl(_))
    objs.foreach(analyzeObjectMemberDecl(_))
    cls.foreach(analyzeClassMember(_))
    objs.foreach(analyzeObjectMember(_))
  }
  
  
  
  /**
   * <p>
   * 	Check the objects definition.
   * </p>
   *
   * @param objects: The list of object definition in  the source program.
   */
  protected def analyzeObjectDecl(objects: List[ObjectDef]) {
    for(ObjectDef(name,inv,methods)<-objects) {
      val objSym = ObjectSymbol(name.getName)
      name.sym = objSym
      object_scope.update(name.getName,objSym)
    }
  }
  
  
  
  /**
   * <p>
   *	Check the classes definition.
   * </p>
   *
   * @param declared_classes: The list of names declared classes.
   * @param klasses         : The list of classes definition. 
   */
  protected def analyzeClassesDecl(declared_classes: List[String], klasses: List[ClassDef]) {
    klasses match {
      case Nil => ()
      case (klass @ ClassDef(_,isAbs,name,fields,superclass,invariant,methods)) :: rest =>
        // Check if declared class is already in the scope
        class_scope.get(name.getName) match {
          case Some(defined) => 
            Report.warning(klass.pos, "Duplicate class '"+name.getName+"'")

          case None =>
            var superSym : Option[ClassSymbol] = None
            // Superclass belong to the list of declared classes
            if(superclass.isDefined) {
              val superc = superclass.get
              class_scope.get(superc) match {
                case None if !declared_classes.contains(superc) => 
                  Report.warning(klass.pos,"Superclass '"+name+"' is unknown")
                case None => return analyzeClassesDecl(declared_classes,rest ::: List(klass))  
                case Some(sym) => superSym = Some(sym) // then the current class declaration is fine
              }
            }
            name.sym = ClassSymbol(isAbs,name.getName,invariant,superSym)
            class_scope + ((name.getName,name.sym))
            analyzeClassesDecl(declared_classes,rest)
        }
    }
  }

  
  /**
   * <p>
   *	Check the member declaration of a class.
   * </p>  
   *
   * @param klass: The class whose members declaration have to be checked.
   */  
  private def analyzeClassMemberDecl(klass: ClassDef) {
    val ClassDef(_,_,name,fields,_,_,methods) = klass
    fields.foreach(analyzeField(name.sym,_))
    methods.foreach(analyzeMethodDecl(name.sym,_))
  }
  
  
  
  /**
   * <p>
   *	Check member definition of a class.
   * </p>
   *
   * @param klass: The class whose member definition have to be checked.
   */
  private def analyzeClassMember(klass: ClassDef) {
    val ClassDef(_,_,name,fields,_,_,methods) = klass
    methods.foreach(analyzeMethodBody(name.sym,_))
  }
    

  
  /**
   * <p>
   *	Check the members declaration of an object.
   * </p>
   *
   * @param obj: The object whose member declaration have to be checked.
   */
  private def analyzeObjectMemberDecl(obj: ObjectDef) {
    val ObjectDef(name,_,methods) = obj
    methods.foreach(analyzeMethodDecl(name.sym,_))	  
  }
  
  
  
  /**
   * <p>
   *	Check the members definition of an object.
   * </p>
   *
   * @param obj: The object whose member have to be checked.
   */
  private def analyzeObjectMember(obj: ObjectDef): Unit = {
    val ObjectDef(name,_,methods) = obj
    methods.foreach(analyzeMethodBody(name.sym,_))          
  }
  

   
   
  // ------------------- Analyze Members ---------------------
  
    
  protected def analyzeField(klass: ClassSymbol, field: FieldDef): Unit = {
    val FieldDef(name,tpe) = field
    klass.lookupField(name.getName) match {
      case Some(_) => Report.warning(field.pos,"In class "+klass.getName+", field '"+name.getName+"' is already defined in the scope");
      case None => 
        val fieldName = new Name(name.getName,klass)
        val fieldSymbol= FieldSymbol(fieldName, analyzeType(tpe));
        name.sym = fieldSymbol
        klass.enterField(fieldSymbol)
    }
  }
  
  
  
  
  
  protected def analyzeMethodDecl(container: TypeSymbol, method: MethodDef): Unit = {
    val MethodDef(mname,params,restpe,ppc,body) = method
    
    val metResType = analyzeType(restpe);
    //val par =  (params.map(_.name),params.map(_.tpe))
    var paramtypes = params.map(p => (p.name.getName,analyzeType(p.tpe)));
    val methodname = new Name[TypeSymbol](mname.getName,container)
    var methodSymbol: MethodSymbol = MethodSymbol(methodname, paramtypes, metResType,ppc);
        
    
    container.lookupMethod(mname.getName) match {
      case Some(met) =>
        //Check size parameters
        if (paramtypes.length != met.params.length) 
                Report.warning(method.pos,"Wrong number of arguments for method "+mname.getName);
        else {
          //Check types method
          val comp = met.params.map(_._2).zip(paramtypes.map(_._2));
          var ok = true;
          if(!metResType.isSubtype(met.resTpe)) {
            ok = false;
            Report.warning(method.pos,"Type mysmatch. Expected return Type '"+metResType+"', found '"+met.resTpe+"'");
          }
          if(comp.exists(pair=> !pair._1.isSubtype(pair._2))) { 
            Report.warning(method.pos,"Method '"+mname.getName+"' can't be overloaded, type of parameters is not well formed") 
            ok = false;
          }
          if(ok) {
            container.enterMethod(methodSymbol);
            mname.sym = methodSymbol;
          }
        }

      case None => 
        container.enterMethod(methodSymbol);
        mname.sym = methodSymbol;
    }
  }   
  
  
  
  protected def analyzeMethodBody(container: TypeSymbol, method: MethodDef): Unit = {
    val MethodDef(mname,params,restpe,ppc,body) = method
    val methodSymbol = mname.getSym
    
    // Analyze body
    var varscope = empty_var_scope
    for(param<-params) {
      val ParamDef(pname,tpe) = param
      varscope.get(pname.getName) match {
        case Some(v) => Report.warning(param.pos, "Parameter '"+v+"' is defined twice in method '"+mname.getName+"'")  
        case None => 
          val paramSymbol = ParamSymbol(new Name(pname.getName,methodSymbol),analyzeType(tpe))
          pname.sym = paramSymbol
          varscope = varscope.update(pname.getName,paramSymbol)
      }
    }
    
    val selfSymbol = container match {
      case c: ClassSymbol => ParamSymbol(new Name(method.self,methodSymbol),IClassType(c))
      case o : ObjectSymbol => ParamSymbol(new Name(method.self,methodSymbol),IObjectType(o))
    }
    
    varscope = varscope.update(method.self,selfSymbol)
    val bodyTpe = analyzeExpr(varscope,body)
    if(!bodyTpe.isSubtype(analyzeType(restpe)))
      Report.warning(method.pos,"In method '"+mname.getName+"', body type is "+bodyTpe+" not conform to the method return type "+restpe)
      
  }
  
  
  
  protected def analyzeExpr(varscope: VarScope, expr: Expr): Type = (expr : @unchecked) match {
    case ObjectId(name) =>
      object_scope.get(name.getName) match {
        case Some(sym) =>
          name.sym = sym
          IObjectType(sym)
        case None =>
          Report.warning(expr.pos,"Object "+name.getName+" has not been declared")
          IBadType
      }
      
    
    case Ident(name) =>
      varscope.get(name.getName) match {
        case Some(sym) => 
          name.sym = sym
          sym.getTpe
        case None =>
          /*object_scope.get(name.getName) match {
            case Some(objSym) =>
              IObjectType(objSym)
            case None => 
              Report.warning(expr.pos,"variable "+name.getName+" has not been declared")
              IBadType
          }*/
          Report.warning(expr.pos,"variable "+name.getName+" has not been declared")
          IBadType
      }    
    
    case True | False => IBoolType
    
    case Call(obj,method,args) => 
      val typeSym: TypeSymbol = analyzeExpr(varscope,obj) match {
        case IClassType(c) => c
        case IObjectType(o) => o
        case err @ _ => 
          //Report.warning(expr.pos,"Method '"+method.getName+"' is not declared in class/object '"+err+"'")
          for(arg<-args) analyzeExpr(varscope,arg) 
          return IBadType
      }
      //method.sym.name.sym = typeSym
      
      typeSym.lookupMethod(method.getName) match {
        case None => 
          Report.warning(expr.pos, "Class/Object "+typeSym.getName+" does not contain method "+method.getName)
           val argstpe = for(i<-List.range(0,args.size)) yield{
             (i+"",analyzeExpr(varscope,args(i)))
           }
          method.sym = MethodSymbol(new Name(method.getName,typeSym),argstpe,null,None)
          IBadType
        case Some(msym) =>
          method.sym = msym
          (msym.params.length == args.length) match {
            case false =>
              Report.warning(expr.pos, "Wrong number of arguments for method "+method.getName+" of class/object "+typeSym.getName)
              IBadType
            case true => 
              for((paramTpe,arg)<-msym.params.zip(args)) {
                if(!analyzeExpr(varscope,arg).isSubtype(paramTpe._2)) {
                  Report.warning(expr.pos, "In term "+expr+", argument "+arg+" is not a subtype of method parameter type "+paramTpe)
                  return IBadType
                }
              }
          }
          msym.resTpe
      }
    
      
    case Select(obj,field) =>
      analyzeExpr(varscope,obj) match {
        case IClassType(classSym) =>
          classSym.lookupField(field.getName) match {
          case None => 
            Report.warning(expr.pos, "Field "+field.getName+" has not been declared in class "+classSym.getName)
            IBadType
          case Some(fieldSym) =>
            field.sym = fieldSym
            fieldSym.fieldTpe
        }
      case err @ _ => 
        Report.warning(expr.pos, "Can't dereference type "+err) 
        return IBadType
      }
    
    
    case If(cond,then,elze) =>
      analyzeExpr(varscope,cond) match {
        case IBoolType => 
          (analyzeExpr(varscope,then),analyzeExpr(varscope,elze)) match {
            case (thenTpe,elzeTpe) =>
              thenTpe.lub(elzeTpe) match {
                case None =>
                  Report.warning(expr.pos, "Then branch has type "+thenTpe+", else branch has type "+elzeTpe+". No common lub.")
                  IBadType
                  
                case Some(lub) => lub
              }  
          }
        case _ => IBadType
      }
      
    case New(name,args) =>
      class_scope.get(name.getName) match {
        case None => 
          Report.warning(expr.pos,"Class "+name.getName+" has not been declared.")
          return IBadType
        case Some(sym) => 
          name.sym = sym
          val fields = sym.allFields
          (fields.length == args.length) match {
            case false =>
              Report.warning(expr.pos,"Wrong number of arguments for call to constructor of class "+name.getName)
              return IBadType
            case true =>
              for((field,arg)<-fields.zip(args)) {
                if(!analyzeExpr(varscope,arg).isSubtype(field.fieldTpe)) {
                  Report.warning(expr.pos,"In term "+expr+", argument "+arg+
                                 " is not a subtype of field "+field+" of type "+field.fieldTpe)
                  return IBadType
                }
              }
              return IClassType(sym)
          }
      }
      
    case IsInstanceOf(expr,tpe) => 
      analyzeExpr(varscope,expr) match {
        case IBadType =>
          IBadType
        case _ => analyzeType(tpe) match {
          case IBadType => IBadType
          case _ => IBoolType
        }
      }
       
    case m : Match => analyzeMatchExpr(varscope,m)
      
    case StringLit(_) => IStringType
    
    case Number(_) => IIntType
    
    case ArithBinaryExpr(left,right) => 
      (analyzeExpr(varscope,left),analyzeExpr(varscope,right)) match {
        case (IIntType,IIntType) => IIntType
        case _ => IBadType
      } 
      
    case BoolBinaryExpr(left,right) =>
      (analyzeExpr(varscope,left),analyzeExpr(varscope,right)) match {
        case (IBoolType,IBoolType) => IBoolType
        case _ => IBadType
      }
  
    case !(expr) => analyzeExpr(varscope,expr) match {
      case IBoolType => IBoolType
      case _ => IBadType
    }
    
    case CmpBinaryExpr(left,right) =>
    (analyzeExpr(varscope,left),analyzeExpr(varscope,right)) match {
      case (ltpe,rtpe) if ltpe == rtpe => IBoolType
      case _ => IBadType
    }
  
  }
  
  protected def analyzeMatchExpr(varscope: VarScope, pm: PatternMatching): Type = pm match {
    case m @ Match(scrutinee,tpe,cases) =>
      var lub : Option[Type] = None
      (analyzeExpr(varscope,scrutinee),analyzeType(tpe)) match {
        case (found,expected) if found.isSubtype(expected) =>
          for(kase<-cases) {
            val caseTpe = analyzeMatchExpr(varscope,kase)
            if(lub.isEmpty) {
              lub = Some(caseTpe)
            } else {
              val temp = caseTpe.lub(lub.get)
              if(temp.isEmpty) {
                Report.warning(kase.pos,"Expected type "+lub.get+", found "+caseTpe)
                return IBadType
              }
              lub = temp
            }
          }
          return lub.get
        case (found,expected) =>
          Report.warning(m.pos,"Found type "+found+". Expected "+expected)
          for(kase<-cases) analyzeMatchExpr(varscope,kase)
          IBadType
      }
      
    case cd @ CaseDef(_,pattern,guard,body) =>
      def analyzePattern(varscope: VarScope, pattern: PatternDef): List[(String,VarSymbol)] = pattern match {
        case VarPattern(x,tpe) =>
          val locSym = LocalSymbol(x.getName,analyzeType(tpe))
          x.sym = locSym
          List((x.getName,locSym))
        
        case Wildcard => Nil
        case ClassPattern(name,args) =>
          object_scope.get(name.getName) match {
            case None => 
              Report.warning(cd.pos, "Object "+name.getName+" has not been declared")
              Nil ///XXX: Should throw an exception
            
            case Some(objSym) =>
              name.sym = objSym
              objSym.lookupMethod(UNAPPLY) match {
                case None =>
                  Report.warning(cd.pos,"Object "+name.getName+" does not contains unapply method")
                  Nil
                case Some(MethodSymbol(_,paramsTpe,resTpe,_)) =>
                  // XXX: Should check that the internal structure is type correct!!
                  //XXX: Should also check that we don't have two variables with same name
                  args.map(analyzePattern(varscope,_)).flatMap(p=>p)  
              }
          }
      }
      val update = analyzePattern(varscope,pattern)
      if(guard.isDefined) {
        analyzeExpr(varscope ++ (update),guard.get) match {
          case IBoolType => ()
          case _ => 
            Report.warning(cd.pos, "Guard of case "+pm+" expected to be of type Bool")
            return IBadType
        }
      }
      
      analyzeExpr(varscope ++ update,body)
  }
  
  protected def analyzeType(tpe: TypeTree): Type = tpe match {
    case IntType => IIntType
    case BoolType => IBoolType
    case OptionType(tpes) => 
      IOptionType(tpes.map(tp => analyzeType(tp)))
    case ParamType(stpe,ptpe) =>
      IParamType(analyzeType(stpe),analyzeType(ptpe))
    case ClassType(klass_name) => 
      class_scope.get(klass_name) match {
        case Some(c) =>
          IClassType(c)
        case None => 
          Report.warning(tpe.pos, "Class " + klass_name + " is unknown");
          IBadType
      }
    
  }
  
  
  
}
